package com.jingdianjichi.interview.server.utils;

import com.alibaba.fastjson.JSON;
import com.alibaba.nacos.common.utils.MapUtils;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.cookie.BasicCookieStore;
import org.apache.hc.client5.http.cookie.CookieStore;
import org.apache.hc.client5.http.impl.DefaultHttpRequestRetryStrategy;
import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
import org.apache.hc.client5.http.socket.ConnectionSocketFactory;
import org.apache.hc.client5.http.socket.PlainConnectionSocketFactory;
import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory;
import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.http.HttpStatus;
import org.apache.hc.core5.http.ParseException;
import org.apache.hc.core5.http.config.Registry;
import org.apache.hc.core5.http.config.RegistryBuilder;
import org.apache.hc.core5.http.io.SocketConfig;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.http.io.entity.StringEntity;
import org.apache.hc.core5.util.TimeValue;

import java.io.*;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.stream.Collectors;

/**
 * @Author: ChickenWing
 * 和大模型交互的时候我们用的都是http请求的方式,这个工具类就是帮我们快速建立请求的亲
 */
@Slf4j
public class HttpUtils {

    private HttpUtils() {
    }

    /**
     * HttpClient 对象    //我们要保证httpclient是单列的,否则每次建立连接的时候是非常耗时的
     *  这个就是客户端
     */
    private static CloseableHttpClient httpClient = null;

    /**
     * CookieStore 对象
     */
    private static CookieStore cookieStore = null;


    /**
     * Basic Auth 管理对象
     **/
    private static BasicCredentialsProvider basicCredentialsProvider = null;


    /**
     * HttpClient初始化,保证单列
     **/
    static {
        // 注册访问协议相关的 Socket 工厂,用于管理 HTTP 和 HTTPS 连接。
        Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create()
                .register("http", PlainConnectionSocketFactory.getSocketFactory())      //用于处理普通的 HTTP 连接。
                .register("https", SSLConnectionSocketFactory.getSocketFactory())       //用于处理安全的 HTTPS 连接
                .build();

        //Http 连接池
        //PoolingHttpClientConnectionManager 是一个支持连接池的连接管理器，可以管理多个连接。
        //使用之前创建的 Socket 工厂注册表，以确保可以处理 HTTP 和 HTTPS 连接。
        PoolingHttpClientConnectionManager poolingHttpClientConnectionManager = new PoolingHttpClientConnectionManager(registry);

        /*
            SocketConfig 是对 socket 连接的配置。
                setSoTimeout(15, TimeUnit.SECONDS) 设置读超时时间为 15 秒。
                setTcpNoDelay(true) 禁用 Nagle 算法以提高小数据包的发送效率。
         */
        poolingHttpClientConnectionManager.setDefaultSocketConfig(SocketConfig.custom()
                .setSoTimeout(15, TimeUnit.SECONDS)             //超时时间
                .setTcpNoDelay(true).build()
                //SocketConfig 主要与底层连接的行为相关（如读取数据的超时等），适用于通过连接池建立的所有连接。
        );

        // 整个连接池最大连接数
        poolingHttpClientConnectionManager.setMaxTotal(1000);

        // 每路由最大连接数，默认值是2,设置每个路由的最大连接数为 200
        poolingHttpClientConnectionManager.setDefaultMaxPerRoute(200);

        //当连接在连接池中闲置（即没有被使用）超过 15 秒后，才会进行有效性验证
        poolingHttpClientConnectionManager.setValidateAfterInactivity(TimeValue.ofSeconds(15));

        // Http 请求配置
        RequestConfig requestConfig = RequestConfig.custom()
                .setConnectTimeout(5000, TimeUnit.MILLISECONDS)                         //设置连接超时时间为 5000 毫秒（5 秒）。
                .setResponseTimeout(5000, TimeUnit.MILLISECONDS)                       //设置响应超时时间为 5000 毫秒（5 秒）
                .setConnectionRequestTimeout(5000, TimeUnit.MILLISECONDS)       //置从连接池获取连接的超时时间为 5000 毫秒（5 秒）。
                .build();
        //RequestConfig 则是针对具体的 HTTP 请求的配置，包括建立连接、响应以及从连接池获取连接等方面的超时设置
        //这两个配置组合在一起，可以帮助开发者更精细地控制 HTTP 客户端的各个方面，提高性能和稳定性



        // 设置 Cookie
        /*
            BasicCookieStore 是 Apache HttpClient 中的一个实用类，用于存储和管理 HTTP cookies。
            创建一个新的 BasicCookieStore 实例，用于在会话期间存储 cookies，方便后续的请求使用，如处理身份验证或用户会话等。
         */
        cookieStore = new BasicCookieStore();
        // 设置 Basic Auth 对象
        /*
            BasicCredentialsProvider 用于存储 HTTP 认证所需的凭证（如用户名和密码）。
            可以将用户的认证信息设置到此对象中，以便为后续的请求提供身份验证。当访问需要认证的资源时，HttpClient 将会自动使用这些凭证。
         */
        basicCredentialsProvider = new BasicCredentialsProvider();

        // 创建监听器，在 JVM 停止或重启时，关闭连接池释放掉连接
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            try {
                log.info("HttpClientUtil.beginClose!");
                httpClient.close();
                log.info("HttpClientUtil.close");
            } catch (IOException e) {
                log.error("HttpClientUtil.close.error:{}", e.getMessage(), e);
            }
        }));

        // 创建 HttpClient 对象
        httpClient = HttpClients.custom()
                // 设置 Cookie
                .setDefaultCookieStore(cookieStore)
                // 设置 Basic Auth
                .setDefaultCredentialsProvider(basicCredentialsProvider)
                // 设置 HttpClient 请求参数
                .setDefaultRequestConfig(requestConfig)
                // 设置连接池
                .setConnectionManager(poolingHttpClientConnectionManager)
                // 设置定时清理连接池中过期的连接用于在每次发送请求时检查并移除连接池中已过期的连接。
                .evictExpiredConnections()
                //这个方法设置了清理闲置连接的策略。它会在连接闲置超过 15 秒后，将这些连接从连接池中移除。
                .evictIdleConnections(TimeValue.ofSeconds(15))
                //默认的重试策略表示最多重试 2 次，并且在每次重试时等待 1 秒的超时。这在网络不稳定的情况下，能够提高请求成功的概率
                .setRetryStrategy(new DefaultHttpRequestRetryStrategy(2, TimeValue.ofSeconds(1L)))
                .build();
    }

    /**
     * 获取 Httpclient 对象
     */
    public static CloseableHttpClient getHttpclient() {
        return httpClient;
    }

    /**
     * 获取 CookieStore 对象
     */
    public static CookieStore getCookieStore() {
        return cookieStore;
    }

    /**
     * 获取 BasicCredentialsProvider 对象
     */
    public static BasicCredentialsProvider getBasicCredentialsProvider() {
        return basicCredentialsProvider;
    }

    /**
     * Http get 请求
     */
    public static String httpGet(String uri) {
        if (StringUtils.isBlank(uri)) {
            throw new NullPointerException("HttpClientUtil.uri.notEmpty!");
        }
        String result = "";
        CloseableHttpResponse response = null;
        try {
            // 创建 HttpGet 对象
            HttpGet httpGet = new HttpGet(uri);
            // 执行 Http Get 请求
            response = HttpUtils.getHttpclient().execute(httpGet);
            // 输出响应内容
            if (response.getEntity() != null) {
                result = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
            }
            // 销毁流
            EntityUtils.consume(response.getEntity());
        } catch (IOException | ParseException e) {
            log.error("HttpClientUtil.httpGet.error:{},uri:{}", e.getMessage(), uri, e);
        } finally {
            // 释放资源
            if (response != null) {
                try {
                    response.close();
                } catch (IOException e) {
                    log.error("HttpClientUtil.httpGet.close.error:{}", e.getMessage(), e);
                }
            }
        }
        return result;
    }

    /**
     * Http get 请求
     */
    public static String httpGet(String uri, Map<String, String> cookieMap, Map<String, String> headMap) {
        if (StringUtils.isBlank(uri)) {
            throw new NullPointerException("HttpClientUtil.uri.notEmpty!");
        }

        String result = "";
        CloseableHttpResponse response = null;

        try {
            // 创建 HttpGet 对象
            HttpGet httpGet = new HttpGet(uri);

            //增加cookie
            if (MapUtils.isNotEmpty(cookieMap)) {
                String cookie = cookieMap.entrySet().stream()
                        .map(n -> {
                            try {
                                return n.getKey() + "=" + URLEncoder.encode(n.getValue(), "UTF-8");
                            } catch (UnsupportedEncodingException e) {
                                log.error("HttpClientUtil.httpGet.encode.error!:{}", e.getMessage(), e);
                            }
                            return null;
                        }).collect(Collectors.joining(";"));

                if (StringUtils.isNotBlank(cookie)) {
                    httpGet.addHeader("Cookie", cookie);
                }
            }


            //增加head
            if (MapUtils.isNotEmpty(headMap)) {
                for (String key : headMap.keySet()) {
                    if (StringUtils.isNotBlank(key) && StringUtils.isNotBlank(headMap.get(key))) {
                        httpGet.addHeader(key, headMap.get(key));
                    }
                }
            }

            // 执行 Http Get 请求
            CloseableHttpClient httpclient = HttpUtils.getHttpclient();
            response = httpclient.execute(httpGet);

            // 输出响应内容
            if (response.getEntity() != null) {
                result = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
            }

            // 销毁流
            EntityUtils.consume(response.getEntity());
        } catch (IOException | ParseException e) {
            log.error("HttpClientUtil.httpGet.error:{},uri:{}", e.getMessage(), uri, e);
        } finally {
            // 释放资源
            if (response != null) {
                try {
                    response.close();
                    /*
                        尽管 EntityUtils.consume() 已经消费掉了响应的实体内容，它并不负责关闭响应本身。
                        为了确保所有资源都被适当地释放，你需要在消费实体之后，显式地调用 response.close()。
                     */
                } catch (IOException e) {
                    log.error("HttpClientUtil.httpGet.close.error:{}", e.getMessage(), e);
                }
            }
        }
        return result;
    }

    /**
     * Http Post Json 表单请求示例
     */
    @SneakyThrows
    public static void httpPostJson(String uri, Map<String, Object> reqArgs, Consumer<InputStream> reponseInputStream) {
        CloseableHttpResponse response = null;
        InputStream inputStream = null;
        try {
            if (StringUtils.isBlank(uri)) {
                throw new NullPointerException("HttpClientUtil.httpPostJson.uri.notEmpty!");
            }
            // 创建 HttpPost 对象
            HttpPost httpPost = new HttpPost(uri);

            StringEntity stringEntity = null;
            if (null != reqArgs && 0 != reqArgs.size()) {
                // 将请求对象通过 fastjson 中方法转换为 Json 字符串，并创建字符串实体对象
                stringEntity = new StringEntity(JSON.toJSONString(reqArgs), StandardCharsets.UTF_8);
                // 设置 HttpPost 请求参数
                httpPost.setEntity(stringEntity);
            }
            // 设置 Content-Type
            httpPost.addHeader("Content-Type", ContentType.APPLICATION_JSON);
            // 执行 Http Post 请求
            response = HttpUtils.getHttpclient().execute(httpPost);
            // 输出响应内容
            if (response.getEntity() != null) {
                inputStream = response.getEntity().getContent();
                if (null != inputStream) {
                    log.info("HttpClientUtil.httpPostJson.response.getEntity.getContent.inputStream size:{}", null != inputStream ? inputStream.available() : 0);
                    reponseInputStream.accept(inputStream);
                } else {
                    log.error("HttpClientUtil.httpPostJson.response.getEntity.getContent.inputStream is null,response.code:{}", response.getCode());
                }
            }

            // 销毁流
            if (null != reqArgs && 0 != reqArgs.size() && null != stringEntity) {
                EntityUtils.consume(stringEntity);
            }
            EntityUtils.consume(response.getEntity());
        } catch (Exception e) {
            log.error("HttpClientUtil.httpPostJson.error:{}", e.getMessage(), e);
            throw e;
        } finally {
            // 释放资源
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    log.error("HttpClientUtil.httpPostJson.inputstream.close.error:{}", e.getMessage(), e);
                }
            }
            if (response != null) {
                try {
                    response.close();
                } catch (IOException e) {
                    log.error("HttpClientUtil.httpPostJson.response.close.error:{}", e.getMessage(), e);
                }
            }
        }
    }

    /**
     * post 请求
     */
    public static String executePost(String url, String reqArgs,
                                     Map<String, String> headerMap) {
        String result = null;

        HttpPost httpPost = new HttpPost(url);
        StringEntity stringEntity = new StringEntity(reqArgs, ContentType.create("application/json", "utf-8"));
        httpPost.setEntity(stringEntity);
        if (MapUtils.isNotEmpty(headerMap)) {
            headerMap.forEach((key, val) -> {
                httpPost.setHeader(key, val);
            });
        }
        try {
            CloseableHttpResponse response = HttpUtils.getHttpclient().execute(httpPost);
            if (response.getCode() == HttpStatus.SC_OK || response.getCode() == HttpStatus.SC_CREATED) {
                result = getStreamAsString(response.getEntity().getContent(), "UTF-8");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }

    private static String getStreamAsString(InputStream stream, String charset) throws IOException {
        try {
            BufferedReader reader = new BufferedReader(new InputStreamReader(stream, charset), 8192);
            StringWriter writer = new StringWriter();
            char[] chars = new char[8192];
            int count = 0;
            while ((count = reader.read(chars)) > 0) {
                writer.write(chars, 0, count);
            }

            return writer.toString();
        } finally {
            if (stream != null) {
                stream.close();
            }
        }
    }

}
